組込み現場の「C++」プログラミング 明日から使える徹底入門

高木 信尚(株式会社クローバーフィールド

1.1 C++というのはこんな言語

C++というと,「オブジェクト指向プログラミングに対応したCのスーパーセット言語」という理解をしている方が多いのではないでしょうか.確かに,C++にはオブジェクト指向プログラミングを支援する機能が備わっていますし,おおむねCの機能を包含しているので,この解釈は大きく外れているわけではありません.

しかし,C++は必ずしもオブジェクト指向プログラミングのみに対応しているわけではありませんし,Cの完全な上位互換性を保っているわけでもありません.ここでは,C++というプログラミング言語の概要をざっとご紹介し,C++の全体像を把握していただくことにします.

それではさっそく,C++がどんなプログラミング言語なのか,概要を見ていくことにしましょう.

1.1.1 Cのスーパーセット言語としてのC++

まずは,Cのスーパーセット言語としての側面からです.

もともと,C++のコンパイラは,C++のソースコードをCのソースコードに変換するためのトランスレータとして実装されました.こう書くと,初期のC++コンパイラは高機能なプリプロセッサ程度だったと誤解されるかもしれません.しかし,トランスレータといっても,プリプロセッサのようなマクロ置換を行うだけのものではなく,完全なコンパイラでした.そして,コンパイル結果として機械語やアセンブリ言語を出力する代わりに,Cのソースを出力することで多くのプラットフォームに対応させたのです.

ところで,初期のC++と当時のCとの互換性は,それほど高くありませんでした.関数の定義ひとつ取ってみてもそうです.int型の引数を1つ受け取り,値を返さない関数funcの定義は,当時のCでは次のように書きました.

func(arg)
int arg
{
     …
}

これに対し,C++では次のように書きました.

void func(int arg)
{
     …
}

上記のコードからもわかるように,Cにはもともとvoid型というものが存在しませんでした.また,const修飾子もありませんでした.しかし,私たちが現在使用している標準Cにはそれらがすべて備わっています.これらはもともとC++の言語仕様だったのです.Cの標準規格が制定される段階で,C++のそれらの言語仕様がCにも取り込まれたことで,標準CとC++の間には比較的高い互換性が生じたのです.

それでも,C++がCのスーパーセット言語というのは必ずしも正確とはいえません.実は,むしろ,標準CがC++のサブセット言語として定義され直したというのが実情なのです.実際,C++の設計者であるStroustrup氏の著書『C++の設計と進化』(ソフトバンククリエイティブ刊)では,Cのバイブルともいえる『プログラミング言語C 第2版』(共立出版刊)に掲載されたサンプルコードが,当時のC++コンパイラを用いて検証されたという裏話が語られています.

もちろん,そうはいっても,標準C規格が制定される時点では,すでに古いCで書かれた多くの資産があったため,それらとの互換性を保つために多くの妥協が行われたようです.その結果,標準CはC++の厳格なエラー検出機能を捨てざるをえませんでした.標準CとC++との間の非互換性は,そうしたCの過去のしがらみに関するものが大多数を占めています.

ところで,標準C規格はその後の1999年に改定されました(通称「C99」と呼ばれます).そのとき,前年の1998年に制定された標準C++規格との互換性が大幅に損なわれました*1.しかし,標準C++の次期規格(通称「C++0X」と呼ばれています)では,C99の改定内容の多くをC++にも取り込むことになるようです.このようにして,CとC++は互いに絡み合いながら現在も発展し続けています.

*1 C99対応の処理系も近年ではずいぶん増えてきました.しかし,実際に使用されているケースはまだまだ少ないようです.

1.1.2 オブジェクト指向プログラミング言語としてのC++

C++の代表的な機能といえば,やはりオブジェクト指向プログラミングを支援するためのものでしょう.オブジェクト指向プログラミングそのものについては「第2章 クラスを使いこなそう」で解説しますので,ここではオブジェト指向プログラミングにかかわる言語仕様を単純にご紹介するだけにとどめます.

オブジェクト指向プログラミングでは,データとそれを操作する手続きをひとまとめにして扱います.C++では,「クラス」という一種の型を用いることで,効率良くオブジェクト指向プログラミングを行うことができます.

C++における「クラス」は,Cの構造体の延長となる言語仕様です.Cの構造体は複数のメンバー(データ)を単純に並べただけのものですが,C++のクラスは,個々のメンバーのアクセス制御が可能になります.具体的には,クラス型の変数のメンバーに,誰でも自由にアクセスできるのか,特定の手続きを用いてのみアクセスできるのかを指定できるわけです.そして,アクセスを制限されたメンバーを操作するための「特定の手続き」として,関数をメンバーに持つことができます.なお,C++では,構造体や共用体は一種のクラスとして扱われます.

次の例では,ごく簡単なAという名前のクラスを定義し,それを利用しています.クラスAには,methodという関数のメンバー(メンバー関数)と,data_という変数のメンバー(データメンバー)を定義しています.そして,publicやprivateというキーワードを用いて,アクセス制限を記述しています.ここでは,クラスの雰囲気だけをつかんでください.

【クラスの例】

class A
{
public:
    int method(int arg);
private:
    int data_;
};
int main()
{
    A a;
    a.data_ = 123; // ← エラー! data_はprivateなのでアクセスできない
    a.method(123); // ← メンバー関数の呼び出し 
    return 0;
}

オブジェクト指向プログラミング自体は,Cのような,いわゆるオブジェクト指向プログラミング言語ではないプログラミング言語でも可能です.しかし,C++のクラスのように,オブジェクト指向プログラミングを支援する機能を使うことで,効率良くオブジェクト指向プログラミングを行うことができます.

1.1.3 ジェネリックプログラミング言語としてのC++

読者の中には,「ジェネリックプログラミング」という言葉を初めて目にするという方も少なくないかと思います.ジェネリックプログラミングというのは,一言でいえば,データ型に依存しないプログラミング手法のことです(genericには,「一般的な,包括的な」という意味があります).こう書いても,ピンとくる方は少ないかもしれません.

実は,Cでも,データ型に依存しないプログラミングというのは,比較的普通に行われています.たとえば,次のコードは,2つの値を渡して大きいほうの値を返すマクロですが,おそらくCの経験がある程度ある方であれば,一度ぐらいはどこかで見かけたことがあるか,自分で書いた経験があるのではないでしょうか?

#define max(a, b)  ((a) >= (b) ? (a) ? (b))

このmaxマクロは,引数のデータ型には依存していません.関係演算子>=が使える型であれば何でもよいのです.すなわち,char型でもint型でもdouble型でもかまいませんし,ポインタ型でさえ渡すことができます.これが,ジェネリックプログラミングの考え方の出発点となります.

CのマクロはC++でも使えるので,上のmaxマクロのようなこともできます.しかし,マクロは単に字面の置き換えを行うだけの機能であり,あまり凝ったことはできません.また,副作用にからむ問題などもあり,あまり安全とはいえません.

実際,マクロでなければ実現できないような場合を除き,C++ではマクロは使わないのが原則です(コラム「マクロの使用は控えよう」参照).そこで,C++には,「テンプレート」というジェネリックプログラミングを支援するための言語仕様が備わっています.テンプレートというのは,文字どおり,プログラムのひな形を作るための機能です.百聞は一見にしかずということで,先ほどのmaxマクロをテンプレートを使って書き直すと,次のようになります.

template <typename T>
T max(T a, T b)
{
    return a >= b ? a : b;
}

このコードには,「template 」という見慣れないものが頭に付いてはいますが,明らかに関数です.関数ですので,マクロにあるような副作用にからむ問題はありません.また,この例では単純すぎてわからないかもしれませんが,関数内部でループを使うなど,もっと複雑なこともできるようになります.

別の例もご紹介しましょう.Cを使ったプログラミングでは,線形リストを使うことがよくあるかと思います.たとえば,次のような自己参照構造体を定義して,これを数珠つなぎにするというものです.

struct node
{
    int value;
    struct node *next;
};

しかし,このnode構造体の場合,int型のvalueというデータに依存しています.異なるデータを扱う場合には別の構造体を定義しなければならず,その構造体を操作するための関数もまた,個別に定義しなければなりません.

このような場合にジェネリックプログラミングを適用すると,次のようにあらゆる型に対応した線形リストを記述することができるようになります.

template <typename T>
struct node
{
    T value;
    struct node *next;
};
template <typename T>
void insert(struct node<T> *n1, struct node<T> *n2)
{
    n2->next = n1->next;
    n1->next = n2;
}
int main()
{
    struct node<int> n1 = { 1 };
    struct node<int> n2 = { 2 };
    insert(&n1, &n2);
    return 0;
}

このサンプルコードでは,main関数の中でnodeのデータ型としてintを指定していますが,構造体なども含めて,どんな型でも自由に指定することができます.そして,どんな型を指定したとしても,insert関数はそのまま利用することができるのです.

実はCでも,型に依存しない線形リストを見かけることがあります.ジェネリックプログラミングもC++の専売特許ではなくCでも可能なのですが,それらはデータをvoid*として保持していたり,強引なキャストに依存したりしているため,静的な型チェックがまったく期待できません.C++では,テンプレートという言語仕様によって,安全かつ効率的にジェネリックプログラミングを行うことが可能なのです.

1.1.4 C++はどんなプログラミング手法も強制しない

これまで見てきたように,C++は,Cと同じ構造化プログラミングのほか,オブジェクト指向プログラミングやジェネリックプログラミングを支援するための機能が備わっています.これらは手法として直交するものであり,現場の事情や必要に応じて,望みの手法を選択することが可能です.もちろん,複数の手法を混在させることも可能です.C++自体は,どの手法を採用すべきかを強制することはありません.

たとえば,オブジェクト指向プログラミングの手法を強制する他の言語を用いた場合,オブジェクト指向プログラミングを行わなければ,それは邪道ということになるでしょう.しかし,C++に限っては,オブジェクト指向プログラミングを行わなければならないということはないのです.Cと同じスタイルでプログラミングするのも,あるいはJava風のスタイルでプログラミングするのも,何も問題ありません.どんな機能を使えるかだけでなく,必要に応じて,C++が持っている機能を「使わない」という選択ができるのも,大きなメリットなのです*2

*2 ガベージコレクションの機能は便利ですが,それを使わないという選択ができない言語は非常に不便です.